/**
 * APITable <https://github.com/apitable/apitable>
 * Copyright (C) 2022 APITable Ltd. <https://apitable.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Twitter_Snowflake
 * snowflake algorithm
 *
 * SnowFlake structure: (64bits, each part is separated by `-'s):
 *   0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 *   |   ----------------------|----------------------   --|--   --|--   -----|------
 * 1bit-useless       41bit timestamp                  10bit-machine id    sequence id
 *
 * - 1-bit sign.
 *      In 2's complement representation, the highest bit of an integer is the sign, but we usually use integers to generate IDs,
 *      so the highest bit is always 0.
 * - 41-bit timestamp (in milliseconds):
 *      Note that the 41-bit timestamp is not the timestamp of the current time,
 *      it is the difference between the real timestamp and the predefined starting timestamp.
 *      The start timestamp here is usually the time our ID generator starts being used,
 *      as specified by the constant `START_EPOCH' in the code below.
 *      The 41-bit timestamp can be used for 69 years, T = (1L << 41)/(1000L * 60 * 60 * 24 * 365) ≈ 69.
 * - 10-bit machine ID that identifies the current machine, including 2-bit data center ID and 8-bit worker ID.
 *      In theory, the server can be deployed on 1024 machines at most.
 * - 5-bit sequence, the sequence number within one millisecond.
 *      5-bit sequence number supports each node to generate 32 IDs every millisecond (the same machine, the same timestamp).
 * - Adds up to just 64 bits, a long integer.
 *
 * The advantages of SnowFlake:
 * - Overall sorted by time increment
 * - and no ID collisions occur throughout the distributed system (distinguished by data center ID and worker ID)
 * - And it's efficient, with tests showing SnowFlake producing around 260,000 ids per second.
 *
 * Open Source Project：https://gitee.com/yu120/sequence
 */

import getMAC from 'getmac';

// Bit division [data center ID (2bit), worker ID (8bit)] (support 1024 nodes in total), sequence ID (5bit)
const WORKER_ID_NUM_BITS = 8n;
const DATA_CENTER_ID_NUM_BITS = 2n;
const MACHINE_ID_NUM_BITS = WORKER_ID_NUM_BITS + DATA_CENTER_ID_NUM_BITS;
const SEQUENCE_NUM_BITS = 5n;

const MACHINE_ID_MASK = (1n << (DATA_CENTER_ID_NUM_BITS + WORKER_ID_NUM_BITS)) - 1n;
const SEQUENCE_MASK = (1n << SEQUENCE_NUM_BITS) - 1n;
export const TIMESTAMP_LEFT_SHIFT = MACHINE_ID_NUM_BITS + SEQUENCE_NUM_BITS;

// Starting time (2018-02-01), you can set the time when you start using the system, and you can use it for 69 years
const START_EPOCH = 1548988646430n;

class SnowFlake {
  private readonly machineBits: bigint;

  sequence: bigint;
  lastTimestamp: bigint;

  /**
   * constructor, running in memory
   */
  constructor() {
    const machineId = this.getMachineId();
    this.machineBits = machineId << SEQUENCE_NUM_BITS;
    this.sequence = 0n;

    console.log(`Initialized snowflake: machine ID: 0x${machineId.toString(16).padStart(3, '0')}`);

    // The last time the ID was generated (this is in memory? what would happen after the system clock returns or the reboot?)
    this.lastTimestamp = -1n;
  }

  /**
   * Get the next ID (this method is thread safety)
   *
   * @returns {bigint} SnowflakeId return id
   */
  nextId(): bigint {
    let timestamp = this.timeGen();
    // If the current time is less than the timestamp generated by the last ID,
    // it means that the system clock should be returned at this time, should throw error
    const diff = timestamp - this.lastTimestamp;
    if (diff < 0n) {
      throw new Error(`Clock moved backwards. Refusing to generate id for ${-diff} milliseconds`);
    }

    // if they were generated at the same time, then sequence them in milliseconds
    if (diff === 0n) {
      // tslint:disable-next-line: no-bitwise
      this.sequence = (this.sequence + 1n) & SEQUENCE_MASK;
      // sequence overflow in milliseconds
      if (this.sequence === 0n) {
        // block it until the next millisecond, get a new timestamp
        timestamp = this.tilNextMillis(this.lastTimestamp);
      }
    } else {
      // Time stamp changes, the sequence is reset in milliseconds
      this.sequence = 0n;
    }

    // Save the timestamp for the last generation ID
    this.lastTimestamp = timestamp;

    return ((timestamp - START_EPOCH) << TIMESTAMP_LEFT_SHIFT) | this.machineBits | this.sequence;
  }

  /**
   * Block to the next millisecond until it gets a new timestamp
   *
   * @param {*} lastTimestamp The timestamp of the last ID generation
   * @returns {bigint}
   * @memberof SnowFlake
   */
  tilNextMillis(lastTimestamp: bigint): bigint {
    let timestamp = this.timeGen();
    while (timestamp <= lastTimestamp) {
      timestamp = this.timeGen();
    }
    return timestamp;
  }

  /**
   * return the current time in milliseconds
   * @return {bigint} current time in milliseconds
   */
  timeGen(): bigint {
    return BigInt(+new Date());
  }

  /**
   * Get the 10-bit machine ID
   */
  private getMachineId(): bigint {
    const MAX_64BIT_UNSIGNED_INT = 18446744073709551615n;
    const FNV_OFFSET_BASIS = 14695981039346656037n;
    const FNV_PRIME = 1099511628211n;

    const hash = getMAC()
      .split(':')
      .map(seg => parseInt(seg, 16))
      // FNV hash
      .reduce((hash, x) => ((hash * FNV_PRIME) & MAX_64BIT_UNSIGNED_INT) ^ BigInt(x), FNV_OFFSET_BASIS);
    return hash & MACHINE_ID_MASK;
  }
}

export const IdWorker = new SnowFlake();
